require "enumerator"
require "rexml/document"

class String
  def java_style
    str = split('_').map{|s| s[0..0].upcase + s[1..-1].downcase }.join
    str[0..0].downcase + str[1..-1]
  end
end

module Maven
  module IgnoreNilElement
    def text_value(*tags)
      text_node = nil
      tags.each do |tag|
        text_node = get_text(tag)
        break if text_node
      end
      text_node ? text_node.value : nil
    end
  end
  
  
  DEFAULT_PROJECT_SETTINGS = "mvn_projects.yml"
  
  def self.create_project(filename, settings = nil)
    if filename.is_a?(Hash)
      Project.new("unnamed", filename)
    elsif /\.xml\Z/i =~ filename
      raise ArgumentError, "#{filename} not found" unless File.exist?(filename)
      open(filename) do |f|
        pom = REXML::Document.new(f)
        pom.elements.each("project") do |proj|
          proj.extend(IgnoreNilElement)
          settings = {
            'rake_prefix' => 'mvn', 
            'pom_dir' => File.dirname(filename).gsub(/[\\\/]/, '_')
          }.update(settings || {})
          settings.update({
            'group_id' => proj.text_value('groupId'),
            'artifact_id' => proj.text_value('artifactId')
            })
          return Project.new(proj.text_value('name', 'artifactId'), settings)
        end
      end
    else
      raise ArgumentError, "unsupported file: #{filename}"
    end
  end
  
  def self.create_projects(settings)
    settings.map do |name, setting|
      Project.new(name, setting)
    end
  end
  
  # maven2 plugins
  # see http://maven.apache.org/plugins/index.html
  PLUGINS = 
    # core plugins
    %w(clean compiler deploy install resources site surefire verifier) + 
    # packaging types / tools
    %w(ear ejb jar rar war shade) + 
    # reporting
    %w(changelog changes checkstyle clover doap docck javadoc jxr pmd project-info-reports surefire-report) +
    # tools
    %w(ant antrun archetype assembly dependency enforcer gpg help invoker one patch plugin release remote-resources repository scm source stage) +
    # IDEs
    %w(eclipse idea) +
    # Outside The Maven Land
    %w(build-helper castor javacc jdepend native sql taglist) +
    # A number of other projects provide their own Maven plugins. This includes
    %w(cargo jaxme jetty jalopy)
  
  # http://maven.apache.org/guides/introduction/introduction-to-the-lifecycle.html
  LIFECYCLES = 
    [
     # Clean Lifecycle
     { :name => "pre-clean", :desc => "executes processes needed prior to the actual project cleaning"},
     { :name => "clean", :desc => "remove all files generated by the previous build"},
     { :name => "post-clean", :desc => "executes processes needed to finalize the project cleaning"},
     # Default Lifecycle
     { :name => "validate", :desc => "validate the project is correct and all necessary information is available."},
     { :name => "generate-sources", :desc => "generate any source code for inclusion in compilation."},
     { :name => "process-sources", :hidden => true, :desc => "process the source code, for example to filter any values."},
     { :name => "generate-resources", :desc => "generate resources for inclusion in the package."},
     { :name => "process-resources", :hidden => true, :desc => "copy and process the resources into the destination directory, ready for packaging."},
     { :name => "compile", :desc => "compile the source code of the project."},
     { :name => "process-classes", :hidden => true, :desc => "post-process the generated files from compilation, for example to do bytecode enhancement on Java classes."},
     { :name => "generate-test-sources", :desc => "generate any test source code for inclusion in compilation."},
     { :name => "process-test-sources", :hidden => true, :desc => "process the test source code, for example to filter any values."},
     { :name => "generate-test-resources", :desc => "create resources for testing."},
     { :name => "process-test-resources", :hidden => true, :desc => "copy and process the resources into the test destination directory."},
     { :name => "test-compile", :desc => "compile the test source code into the test destination directory"},
     { :name => "process-test-classes", :hidden => true, :desc => "post-process the generated files from test compilation, for example to do bytecode enhancement on Java classes. For Maven 2.0.5 and above."},
     { :name => "test", :desc => "run tests using a suitable unit testing framework. These tests should not require the code be packaged or deployed."},
     { :name => "prepare-package", :hidden => true, :desc => "perform any operations necessary to prepare a package before the actual packaging. This often results in an unpacked, processed version of the package. (Maven 2.1 and above)"},
     { :name => "package", :desc => "take the compiled code and package it in its distributable format, such as a JAR."},
     { :name => "pre-integration-test", :hidden => true, :desc => "perform actions required before integration tests are executed. This may involve things such as setting up the required environment."},
     { :name => "integration-test", :desc => "process and deploy the package if necessary into an environment where integration tests can be run."},
     { :name => "post-integration-test", :hidden => true, :desc => "perform actions required after integration tests have been executed. This may including cleaning up the environment."},
     { :name => "verify", :desc => "run any checks to verify the package is valid and meets quality criteria."},
     { :name => "install", :desc => "install the package into the local repository, for use as a dependency in other projects locally."},
     { :name => "deploy", :desc => "done in an integration or release environment, copies the final package to the remote repository for sharing with other developers and projects."},
     # Site Lifecycle
     { :name => "pre-site", :hidden => true, :desc => "executes processes needed prior to the actual project site generation"},
     { :name => "site", :desc => "generates the project's site documentation"},
     { :name => "post-site", :hidden => true, :desc => "executes processes needed to finalize the site generation, and to prepare for site deployment"},
     { :name => "site-deploy", :desc => "deploys the generated site documentation to the specified web server"}
    ]
  
  LIFECYCLE_BIDINGS = {
    # Clean Lifecycle Bindings
    "clean" => "clean:clean",
    # Default Lifecycle Bindings - Packaging ejb / ejb3 / jar / par / rar / war
    "process-resources" => "resources:resources",
    "compile" => "compiler:compile",
    "process-test-resources" => "resources:testResource",
    "test-compile" => "compiler:testCompile",
    "test" => "surefire:test",
    "package" => "ejb:ejb or ejb3:ejb3 or jar:jar or par:par or rar:rar or war:war",
    "install" => "install:install",
    "deploy" => "deploy:deploy",
    # Default Lifecycle Bindings - Packaging ear
    "generate-resources" => "ear:generateApplicationXml",
    "process-resources" => "resources:resources",
    "package" => "ear:ear",
    "install" => "install:install",
    "deploy" => "deploy:deploy",
    # Default Lifecycle Bindings - Packaging maven-plugin
    "generate-resources" => "plugin:descriptor",
    "process-resources" => "resources:resources",
    "compile" => "compiler:compile",
    "process-test-resources" => "resources:testResource",
    "test-compile" => "compiler:testCompile",
    "test" => "surefire:test",
    "package" => "jar:jar and plugin:addPluginArtifactMetadata",
    "install" => "install:install and plugin:updateRegistry",
    "deploy" => "deploy:deploy",
    # Default Lifecycle Bindings - Packaging pom
    "package" => "site:attach-descriptor",
    "install" => "install:install",
    "deploy" => "deploy:deploy",
    # Site Lifecycle Bindings",
    "site" => "site:site",
    "site-deploy" => "site:deploy"
  }
  
  LIFECYCLES.each do |lifecycle|
    if binding = LIFECYCLE_BIDINGS[lifecycle[:name]]
      lifecycle[:binding] = binding
    end
  end
  

  DEFAULT_MVN_PLUGINS_SETTING = "mvn_plugins.yml"
  
  class Project
    attr_accessor :group_id
    attr_accessor :artifact_id

    attr_accessor :name
    attr_accessor :pom_dir
    attr_accessor :rake_prefix
    attr_accessor :plugin_settings
    attr_accessor :verbose

    def initialize(name, settings)
      self.name = name
      self.verbose = false
      settings.each do |key, value|
        send("#{key}=", value)
      end
      @pom_dir||= name.dup
      @rake_prefix ||= "java"
      @plugin_settings ||= DEFAULT_MVN_PLUGINS_SETTING
    end
    
    def plugin_setting_path
      File.join(pom_dir, plugin_settings)
    end
    
    def archetype_create_args
      %w(group_id artifact_id).map do |key|
        "#{key}=#{send(key)}"
      end
    end

    def select_plugins(*args)
      plugins = YAML.load_file(plugin_setting_path).sort
      args.each do |arg|
        regexp = Regexp.new(arg)
        plugins = plugins.select{|plugin| plugin =~ regexp}
      end
      plugins
    end
    
    def execute_without_cd(goal, mvn_args = nil, &block)
      unless mvn_args
        mvn_args = ARGV.dup
        mvn_args.shift
      end
      command = "mvn #{goal} " + mvn_args.map do |arg|
        key, value = *arg.split('=', 2)
        "-D#{key.java_style}=#{value}"
      end.join(" ")
      block ||= lambda{|f| puts f.read }
      puts(command) if verbose
      open("|#{command}", "r", &block)
    end

    def execute_with_cd(goal, mvn_args = nil, &block)
      FileUtils.cd(pom_dir, :verbose => verbose) do
        unless File.exist?("pom.xml")
          raise "You must execute 'rake mvn:create' before #{ARGV.join(' ')}"
        end
        execute_without_cd(goal, mvn_args, &block)
      end
    end

    def execute(goal, mvn_args = nil, &block)
      execute_with_cd(goal, mvn_args, &block)
    end

    def plugin(plugin_name)
      lines = nil
      execute("help:describe", ["plugin=#{plugin_name}", "full=true"]) do |f|
        lines = f.readlines
      end
      lines.each{|line| line.chomp!}
      if lines.any?{|line| /^\ERROR\]/ =~ line}
        raise lines.join
        return nil
      end
      plugin = Plugin.new(plugin_name)
      lines.inject(plugin) do |current, line|
        current.process(line)
      end
      plugin.prepare
      plugin
    end
  end
  
  class Skipper
    def initialize(pattern, source)
      @pattern = pattern
      @source = source
    end
    
    def process(line)
      @pattern =~ line ? @source : self
    end
  end
  
  class Base
    attr_accessor :parent
    
    def skip_to(pattern, status)
      @status = status
      Skipper.new(pattern, self)
    end
    
    def until(line, pattern, next_status, result = nil, &block)
      if pattern =~ line
        @status = next_status
      else
        yield
      end
      result || self
    end
    
    def prepare
      remove_instance_variable(:@status) unless @status.nil?
      @description = (@description || []).select{|line| !line.nil? and line != ""}
    end
  end
  
  class Plugin < Base
    attr_accessor :name, :description
    attr_accessor :group_id, :artifact_id, :version, :goal_prefix
    attr_accessor :goals
    
    def initialize(name)
      @name = name
    end
    
    def prepare
      super
      goals.each{|goal| goal.prepare}
    end
    
    def process(line)
      case @status || :none
      when :none
        skip_to(/^#{'-' * 40}/, :header)
      when :header
        self.until(line, /^Description\:/, :description) do
          key, value = *line.split(':', 2)
          words = *key.split(' ')
          send("#{words.map{|s|s.downcase}.join('_')}=", value.strip)
        end
      when :description
        self.until(line, /^Mojos\:/, :to_mojos) do
          self.description ||= []
          self.description << line.strip
        end
      when :to_mojos
        skip_to(/^#{'=' * 40}/, :goals)
      when :goals
        if /^Goal\: \'(.*)\'/ =~ line
          goal = Goal.new(self, $1)
          self.goals ||= []
          self.goals << goal
          goal
        else
          self
        end
      else
        raise "unimplemented"
      end
    end
  end
  
  class Goal < Base
    attr_accessor :name, :description
    attr_accessor :implementation, :language
    attr_accessor :parameters
    
    def initialize(parent, name)
      @parent = parent
      @name = name.strip
    end

    def prepare
      super
      parameters.each{|parameter| parameter.prepare}
    end
    
    def process(line)
      case @status || :none
      when :none
        skip_to(/^Description\:/, :description)
      when :description
        if /^Implementation\: (.*?)$/ =~ line
          self.implementation = $1.strip
          @status = :language
        else
          self.description ||= []
          self.description << line.strip
        end
        self
      when :language
        key, value = *line.split(": ", 2)
        self.language = value.strip
        @status = :parameters
        skip_to(/^Parameters\:/, :parameters)
      when :parameters
        if line.strip == ""
          self
        elsif /^#{'=' * 40}/ =~ line
          parent
        else
          new_parameter
        end
      else
        raise "unimplemented"
      end
    end
    
    def new_parameter
     parameter = Parameter.new(self)
     self.parameters ||= []
     self.parameters << parameter
     parameter
    end
  end
  
  class Parameter < Base
    attr_accessor :name, :description
    attr_accessor :index, :type, :required, :directly_editable
    
    def initialize(parent)
      @parent = parent
    end
    
    def process(line)
      return parent if /^#{'=' * 40}/ =~ line
      case @status || :none
      when :none
        if /\[(\d+?)\] Name: (.*)/ =~ line
          @index = $1.to_i
          @name = $2.strip
          @status = :attributes
        end
        self
      when :attributes
        self.until(line, /^Description\:/, :description) do
          key, value = *line.split(':', 2)
          words = *key.split(' ')
          send("#{words.map{|s|s.downcase}.join('_')}=", value.strip)
        end
        self
      when :description
        if /^#{'-' * 40}/ =~ line
          parent.new_parameter
        else
          self.description ||= []
          self.description << line.strip
          self
        end
      else
        raise "unimplemented"
      end
    end
    
  end
  
end
